const { Types } = require('mongoose');
const moment = require('moment');
const path = require('path');
const fs = require('fs-extra');
const CodeInstrumentation = require('./mongo/code-instrumentation');
const Func = require('./mongo/function');
const FunctionCFG = require('./mongo/function-cfg');
const File = require('./mongo/file');
const ProjectVersion = require('./mongo/project-version');
const CodeInstrumentationCovered = require('./mongo/code-instrumentation-covered');
const BlackBoxCovered = require('./mongo/black-box-covered');
const CodeInstrumentationResult = require('./mongo/code-instrumentation-result');
const numbers = require('./numbers');
const PhoenixError = require('./phoenix-error');
const utility = require('./utility');
const cacheUtil = require('../cacheUtil');
const filesModels = require('./files');
const functions = require('./functions');
const tests = require('./tests');
const common = require('./common');
const tasks = require('./tasks');
const fileElements = require('./file-elements');
const projectVersions = require('./project-versions');
const invokeRelations = require('./invoke-relations');
const publisher = require('./redis/publisher');
const logger = require('./logger');
const { eventHub, eventKind } = require('./event-hub');

const type = {
  codeInstrumentation: 1,
  targetMachine: 2,
};

const statusKind = {
  frozen: 0, // 代码插桩新建进行中冻结上传结果操作
  finish: 1, // 代码完成插桩
};

const resultStatusKind = {
  processing: 0,
  success: 1,
  fail: 2,
};
/**
 * 创建一个代码插桩
 * @param {string|ObjectID} versionId
 * @param {Array<string|ObjectID>} fileIds
 * @param {Array<string|ObjectID>} functionIds
 * @param {string} description
 * @param {number} type
 * @param {boolean} coverageStatistics
 * @param {string} instrumentWay
 * @param {string} output
 * @returns {Promise<Object>}
 */
async function createCodeInstrumentation(
  versionId,
  fileIds = [],
  functionIds = [],
  description,
  type,
  coverageStatistics,
  instrumentWay,
  output,
) {
  versionId = Types.ObjectId(versionId);
  const version = await ProjectVersion.findById(versionId);
  if (!version) {
    throw new Error(`version not found ${versionId}`);
  }
  fileIds = fileIds.map(Types.ObjectId);
  functionIds = functionIds.map(Types.ObjectId);

  let funcs = [];
  let files = [];
  if (functionIds.length) {
    funcs = await Func.find({ _id: { $in: functionIds } });
    fileIds = fileIds.concat(funcs.map((item) => item.fileId));
  }
  if (fileIds.length) {
    files = await File.find({ _id: { $in: fileIds } });
  }

  const funcMap = funcs.reduce((o, func) => {
    const k = func.fileId.toString();
    o[k] = o[k] || [];
    o[k].push({
      functionId: func._id,
      functionName: func.functionName,
      mangledId: func.mangledId,
      language: func.language,
      kind: func.kind,
    });
    return o;
  }, {});

  const number = await numbers.next(numbers.getCodeInstrumentationNumberName(versionId));

  const ret = await CodeInstrumentation.create({
    creatorId: version.creatorId,
    projectId: version.projectId,
    versionId: version._id,
    files: files.map((file) => ({
      fileId: file._id,
      filePath: file.path,
      status: file.status,
      functions: funcMap[file._id.toString()],
    })),
    number: number.number,
    description,
    type,
    coverageStatistics,
    instrumentWay,
    output,
  });

  return ret;
}

/**
 * 插桩代码列表
 * @param {Object} project 项目信息
 * @param {Object} version 版本信息
 * @returns
 */
async function codeInstrumentationList(project, version) {
  const { projectName } = project;
  const { versionName, _id: versionId } = version;
  let codeInsPrefix = '';
  let codeInsResultPrefix = '';
  let codeIns = await CodeInstrumentation.find({ versionId }).sort({ number: -1 }).lean();
  codeIns = codeIns.map((codeIn) => {
    if (codeIn.type === type.targetMachine) {
      codeInsPrefix = `TestScript_${codeIn.coverageStatistics ? 'IncludeCoverage' : 'ExcludeCoverage'}`;
      codeInsResultPrefix = `TestScript_${codeIn.coverageStatistics ? 'IncludeCoverage' : 'ExcludeCoverage'}`;
    } else {
      codeInsPrefix = 'CodeInstrumentation';
      codeInsResultPrefix = 'CoverageAnalysis';
    }
    const obj = {
      id: codeIn._id,
      name: `${codeInsPrefix}_${projectName}_${versionName}_${codeIn.number}`,
      description: codeIn.description,
      status: codeIn.status,
      createdAt: moment(Types.ObjectId(codeIn._id).getTimestamp()).format('YYYY-MM-DD HH:mm:ss'),
      results: [],
      type: codeIn.type || type.codeInstrumentation,
      instrumentWay: codeIn.instrumentWay || '',
      output: codeIn.output || '',
    };
    codeIn.analyzedResults = codeIn.analyzedResults || [];
    if (codeIn.results) {
      codeIn.results.reverse().forEach((r) => {
        obj.results.push({
          id: r._id,
          name: `${codeInsResultPrefix}_${projectName}_${versionName}_${r.number}`,
          description: r.description,
          status: r.status,
          createdAt: moment(Types.ObjectId(r._id).getTimestamp()).format('YYYY-MM-DD HH:mm:ss'),
          selected: codeIn.analyzedResults.indexOf(r._id.toString()) >= 0,
        });
      });
    }
    return obj;
  });
  return codeIns;
}
/**
 * 编辑插桩代码信息
 * @param {Array[Object]} codeIns 需要修改的内容
 * @param {Object} codeInstrumentation 插桩代码
 * @param {String|ObjectId} codeInstrumentationId 插桩代码id
 * @returns
 */
async function codeInstrumentationEdit(codeIns, codeInstrumentation, codeInstrumentationId) {
  if (!codeIns || codeIns.length === 0) {
    return;
  }
  if (!codeInstrumentation && codeInstrumentationId) {
    codeInstrumentation = await CodeInstrumentation.findById(codeInstrumentationId);
  }
  const codeInResults = [];
  codeIns.forEach((r) => {
    if (r.id) {
      codeInResults.push(r);
      return;
    }
    codeInstrumentation.description = r.description;
  });
  if (codeInResults.length > 0 && codeInstrumentation.results && codeInstrumentation.results.length > 0) {
    for (let i = 0; i < codeInResults.length; i += 1) {
      const r = codeInResults[i];
      for (let j = 0; j < codeInstrumentation.results.length; j += 1) {
        const or = codeInstrumentation.results[j];
        if (or._id.toString() === r.id) {
          or.description = r.description || '';
        }
      }
    }
  }
  await CodeInstrumentation.updateOne({ _id: codeInstrumentation._id },
    {
      $set: {
        description: codeInstrumentation.description,
        results: codeInstrumentation.results,
      },
    });
}
/**
 * 获取插桩代码文件函数树
 * @param {Object} project 当前项目信息
 * @param {Object} codeInstrumentation 当前插桩信息
 * @param {String|ObjectId} codeInstrumentationId 插桩id
 */
async function getCodeInstrumentationTree(project, codeInstrumentation, codeInstrumentationId) {
  const nodeType = {
    file: 1,
    function: 2,
  };
  const tree = {
    id: Types.ObjectId(),
    name: project.projectName,
    kind: filesModels.kind.directory,
    type: nodeType.file,
    children: [],
  };
  if (!codeInstrumentation) {
    codeInstrumentation = await CodeInstrumentation.findById(codeInstrumentationId);
  }
  if (!codeInstrumentation.files || codeInstrumentation.files.length === 0) {
    return tree;
  }
  const codeInFiles = codeInstrumentation.files;
  function buildTree(t, filePaths, number, index) {
    if (!filePaths[number]) {
      const { filePath, fileId } = codeInFiles[index];
      t.id = fileId;
      t.kind = filesModels.kind.file;
      t.name = path.basename(filePath, path.extname(filePath));
      t.type = nodeType.file;
      if (codeInFiles[index].functions) {
        t.children = codeInFiles[index].functions.map((f) => ({
          id: f.functionId,
          name: f.functionName,
          type: nodeType.function,
          kind: f.kind,
        }));
      }
      return;
    }
    let obj = t.children.find((e) => e.name === filePaths[number]);
    if (!obj) {
      obj = {
        id: Types.ObjectId(),
        name: filePaths[number],
        kind: filesModels.kind.directory,
        type: nodeType.file,
        children: [],
      };
      t.children.push(obj);
    }
    number += 1;
    buildTree(obj, filePaths, number, index);
  }
  for (let i = 0; i < codeInFiles.length; i += 1) {
    const number = 0;
    const filePaths = (codeInFiles[i].filePath.split('/')).slice(1);
    buildTree(tree, filePaths, number, i);
  }
  return tree;
}
/**
 *  获取插桩文件源码可用文件路径
 * @param {String|ObjectId} versionId 版本Id
 * @param {String} filePath 文件路径
 * @param {String|ObjectId} codeInstrumentationId  插桩代码id
 * return exist filePath
 */
async function getFileCodeInstrumentationFilePath(versionId, filePath, codeInstrumentationId) {
  filePath = filesModels.getNormalizePath(filePath);
  let codeInstrumentation;
  if (!codeInstrumentationId) {
    [codeInstrumentation] = await CodeInstrumentation.find({ versionId }, ['_id', 'files'])
      .sort({ _id: -1 }).limit(1).lean();
  } else {
    codeInstrumentation = await CodeInstrumentation.findById(codeInstrumentationId).lean();
  }
  if (!codeInstrumentation || !codeInstrumentation.files || codeInstrumentation.files.length === 0) {
    return null;
  }
  const codeInFiles = codeInstrumentation.files.filter((f) => f.filePath === filePath);
  if (codeInFiles.length === 0) {
    return null;
  }
  const codeInstrumentationFilePath = path.join(
    cacheUtil.getCodeInstrumentationDir(),
    versionId.toString(),
    codeInstrumentation._id.toString(),
    filePath
  );
  if (!utility.doesFileExist(codeInstrumentationFilePath)) {
    return null;
  }
  return codeInstrumentationFilePath;
}
/**
 * 获取当前插桩的文件id和函数id
 * @param {String|ObjectId} versionId 版本id
 * @param {String|ObjectId} codeInstrumentationId 插桩代码id
 */
async function getFilesAndFunctions(versionId, codeInstrumentationId) {
  let codeInstrumentation;
  const ids = [];
  if (codeInstrumentationId) {
    codeInstrumentation = await CodeInstrumentation.findById(codeInstrumentationId);
    if (!codeInstrumentation) {
      throw new PhoenixError('not found', `${codeInstrumentationId} not exist !`);
    }
  }
  const codeInstrumentations = await CodeInstrumentation.find({ versionId }, ['_id', 'files'])
    .sort({ _id: -1 }).limit(1).lean();
  if (codeInstrumentations.length === 0) {
    return ids;
  }
  [codeInstrumentation] = codeInstrumentations;
  if (!codeInstrumentation.files || codeInstrumentation.files.length === 0) {
    return ids;
  }
  await utility.arrayChunkOperation(codeInstrumentation.files, 200, async (codeIn) => {
    const file = await File.findOne({ versionId, path: codeIn.filePath }, ['_id']).lean();
    if (!file) {
      return;
    }
    // 整个文件插桩
    if (!codeIn.functions || codeIn.functions.length === 0) {
      ids.push(file._id.toString());
      return;
    }
    // 给文件下某些函数插桩
    const funcs = await Func.find(
      {
        versionId,
        fileId: file._id,
        mangledId: { $in: codeIn.functions.map((c) => c.mangledId) },
      },
      ['_id', 'mangledId']
    ).lean();
    funcs.forEach((f) => {
      ids.push(f._id.toString());
    });
  });
  return ids;
}
function getCodeInstrumentation(codeInstrumentationId) {
  return CodeInstrumentation.findById(codeInstrumentationId);
}

/**
 *  上传结果文件
 * @param {String} codeInstrumentationId 插桩代码Id
 * @param {string} uploadPath 上传文件路径
 * @param {Object} version 版本
 * @param {Object} project 项目
 * @param {Object} creator 创建者
 */
async function uploadResultFile(codeInstrumentationId, uploadPath, version, project, creator) {
  codeInstrumentationId = Types.ObjectId(codeInstrumentationId);
  const destPath = `${uploadPath}.dest`;
  try {
    await utility.uncompress(uploadPath, destPath);
    await fs.remove(uploadPath);
  } catch (err) {
    throw new Error(`Failed to handle uploaded file:${err}`);
  }
  const codeInstrumentation = await getCodeInstrumentation(codeInstrumentationId);
  if (!codeInstrumentation.results) {
    codeInstrumentation.results = [];
  }
  const newCodeInResults = [];
  let namePrefix;
  const resultPath = path.join(cacheUtil.getCodeInstrumentationDir(), version._id.toString(), `${codeInstrumentationId}_results`);
  fs.ensureDirSync(resultPath);
  if (codeInstrumentation.type === type.targetMachine) {
    namePrefix = `TestScript_${codeInstrumentation.coverageStatistics ? 'IncludeCoverage' : 'ExcludeCoverage'}`;
    const resultId = Types.ObjectId();
    const crn = await numbers.next(numbers.getCodeInstrumentationResultsNumberName(version._id.toString(), codeInstrumentationId));
    const result = {
      _id: resultId,
      number: crn.number,
      description: '测试脚本报告',
      status: resultStatusKind.success,
    };
    codeInstrumentation.results.push(result);
    newCodeInResults.push(result);
    const testScriptsResultPath = path.join(resultPath, resultId.toString());
    fs.ensureDirSync(testScriptsResultPath);
    const resultFiles = await fs.readdir(destPath);
    await Promise.all(resultFiles.map(async (fileName) => {
      const filePath = path.join(destPath, fileName);
      if (fs.statSync(filePath).isDirectory()) {
        return Promise.resolve();
      }
      fs.copySync(filePath, path.join(testScriptsResultPath, fileName));
      let fileContent = await utility.readFile(filePath);
      if (!fileContent) {
        return Promise.resolve();
      }
      try {
        fileContent = JSON.parse(fileContent);
      } catch (err) {
        logger.error(`Fail to parse result content to JSON, file: ${filePath}`);
        return Promise.resolve();
      }
      if (!fileContent.testcase) {
        return Promise.resolve();
      }
      const { testCaseIds, testCaseOutputMap } = await tests.formatTestCasesOutput(
        fileContent,
        version,
        fileContent.resultlist !== undefined
      );
      await Promise.all([
        // eslint-disable-next-line no-use-before-define
        storeResultCovered(fileContent, codeInstrumentationId, resultId, version, testCaseIds),
        // eslint-disable-next-line no-use-before-define
        storeResultOutput(testCaseOutputMap, codeInstrumentationId, resultId, version, fileContent.error, fileContent.hash),
      ]);
      return Promise.resolve();
    }));
  } else {
    namePrefix = 'CoverageAnalysis';
    const resultFiles = await fs.readdir(destPath);
    await utility.promiseChain(resultFiles.map((fileName) => async () => {
      const filePath = path.join(destPath, fileName);
      // 忽略mac系统的默认隐藏文件
      if (fileName === '__MACOSX') {
        return;
      }
      // 导入的log文件中不允许有文件夹
      const fileStats = await fs.lstat(filePath);
      if (fileStats.isDirectory()) {
        throw new Error('import zip file must have only one layer of directory');
      }
      // empty file
      if (fileStats.size === 0) {
        return;
      }
      const resultId = Types.ObjectId();
      const crn = await numbers.next(numbers.getCodeInstrumentationResultsNumberName(version._id.toString(), codeInstrumentationId));
      const result = {
        _id: resultId,
        number: crn.number,
        description: '结构覆盖分析报告',
        status: resultStatusKind.processing,
      };
      codeInstrumentation.results.push(result);
      newCodeInResults.push(result);
      fs.copySync(filePath, path.join(resultPath, resultId.toString()));
    }));
  }
  await fs.remove(destPath);
  await CodeInstrumentation.updateOne({ _id: codeInstrumentation._id }, {
    $set: {
      results: codeInstrumentation.results,
    },
  });
  if (codeInstrumentation.type !== type.targetMachine) {
    await tasks.addCodeInstrumentationReportGenTask(codeInstrumentation, newCodeInResults.map(({ _id: id }) => id), project, version, creator);
  }
  return newCodeInResults.map((result) => ({
    id: result._id,
    name: `${namePrefix}_${project.projectName}_${version.versionName}_${result.number}`,
    description: result.description,
    createdAt: moment(result._id.getTimestamp()).format('YYYY-MM-DD HH:mm:ss'),
    status: result.status,
  }));
}

async function removeCodeInstrumentation(codeIns) {
  return Promise.all([
    CodeInstrumentation.deleteOne({ _id: codeIns._id }),
    CodeInstrumentationCovered.deleteMany({ codeInstrumentationId: codeIns._id }),
    CodeInstrumentationResult.deleteMany({ codeInstrumentationId: codeIns._id }),
    fs.remove(path.join(cacheUtil.getCodeInstrumentationDir(), codeIns.versionId.toString(), codeIns._id.toString())),
    fs.remove(path.join(cacheUtil.getCodeInstrumentationDir(), codeIns.versionId.toString(), `${codeIns._id.toString()}_results`)),
  ]);
}

async function removeCodeInstrumentationResult(codeInstrumentationId, resultIds, projectVersion) {
  resultIds = resultIds.map((id) => Types.ObjectId(id));
  codeInstrumentationId = Types.ObjectId(codeInstrumentationId);
  await Promise.all([
    CodeInstrumentation.updateOne({ _id: codeInstrumentationId }, { $pull: { results: { _id: { $in: resultIds } } } }),
    CodeInstrumentationCovered.deleteMany({ codeInstrumentationId, resultId: { $in: resultIds } }),
    CodeInstrumentationResult.deleteMany({ codeInstrumentationId, resultId: { $in: resultIds } }),
  ]);
  // 清除插桩代码结果
  await Promise.all(resultIds.map((resultId) => fs.remove(path.join(
    cacheUtil.getCodeInstrumentationDir(),
    projectVersion._id.toString(),
    `${codeInstrumentationId}_results`,
    resultId.toString()
  ))));
}

async function getResultCovered(codeInstrumentationId, resultId, functionId) {
  const filter = { codeInstrumentationId: Types.ObjectId(codeInstrumentationId), resultId: Types.ObjectId(resultId) };
  if (functionId) {
    filter.functionId = Types.ObjectId(functionId);
  }
  return CodeInstrumentationCovered.find(filter);
}

async function analyzeResults(codeInsAndResults, projectVersion, includeCoverage = true) {
  const projectVersionId = projectVersion._id;
  const filesMap = new Map();
  const functionsMap = new Map();
  const testcasesMap = new Map();
  if (!utility.isArrayType(codeInsAndResults)) {
    return {
      filesMap,
      functionsMap,
      testcasesMap,
      mode: '',
    };
  }
  const functionIds = new Set();
  const fileIds = new Set();
  const resultStatusMap = {};
  let mode = new Set();
  await Promise.all(codeInsAndResults.map(async ({ codeInstrumentationId }) => {
    if (codeInstrumentationId) {
      const codeIns = await getCodeInstrumentation(codeInstrumentationId);
      mode.add(codeIns.type);
      codeIns.files.forEach((file) => {
        fileIds.add(file.fileId.toString());
        file.functions.forEach(({ functionId }) => {
          functionIds.add(functionId.toString());
        });
      });
      codeIns.results.forEach((result) => {
        resultStatusMap[result._id.toString()] = result.status;
      });
    }
  }));
  mode = [...mode];
  // eslint-disable-next-line no-nested-ternary
  mode = mode.length > 1 ? 'mixed' : (mode[0] === type.targetMachine ? 'targetMachine' : 'codeInstrumentation');
  const filesInfo = await filesModels.getFiles(projectVersionId, { fileIds: [...fileIds] });
  filesInfo.forEach((fileInfo) => {
    fileInfo = fileInfo.toObject();
    const extname = common.renameMongoKey(fileInfo.extname);
    // clear coverage info
    fileInfo.statementCovered = { [extname]: 0 };
    fileInfo.branchCovered = { [extname]: 0 };
    fileInfo.mcdcCovered = { [extname]: 0 };
    fileInfo.conditionCovered = { [extname]: 0 };
    fileInfo.decisionCovered = { [extname]: 0 };
    fileInfo.invokeCovered = { [extname]: 0 };
    fileInfo.invokedFuncCovered = { [extname]: 0 };
    fileInfo.statementTotal = { [extname]: 0 };
    fileInfo.branchTotal = { [extname]: 0 };
    fileInfo.mcdcTotal = { [extname]: 0 };
    fileInfo.conditionTotal = { [extname]: 0 };
    fileInfo.decisionTotal = { [extname]: 0 };
    fileInfo.invokeTotal = { [extname]: 0 };
    fileInfo.invokedFuncTotal = { [extname]: 0 };
    fileInfo.functionCount = 0;
    filesMap.set(fileInfo._id.toString(), fileInfo);
  });
  const funcFilter = { versionId: projectVersion._id, kind: { $in: fileElements.getCountableKinds() } };
  if (functionIds.size === 0) {
    funcFilter.fileId = { $in: [...fileIds].map((id) => Types.ObjectId(id)) };
  } else {
    funcFilter._id = { $in: [...functionIds].map((id) => Types.ObjectId(id)) };
  }
  const funcs = await Func.find(funcFilter, ['functionName', 'kind', 'mangledId', 'parents', 'fileId', 'location', 'hash', 'userDefinedFields']).lean();
  funcs.forEach((func) => {
    functionsMap.set(func._id.toString(), {
      _id: func._id,
      functionName: func.functionName,
      mangledId: func.mangledId,
      kind: func.kind,
      fileId: func.fileId,
      parents: func.parents,
      location: func.location,
      userDefinedFields: func.userDefinedFields,
      hash: func.hash,
      // 高层需求覆盖
      coveredFields: {},
      // 低层需求覆盖
      testcasesCoveredFields: {},
      testcases: new Set(),
    });
    if (filesMap.has(func.fileId.toString())) {
      filesMap.get(func.fileId.toString()).functionCount += 1;
    }
  });
  await utility.arrayChunkOperation(
    codeInsAndResults,
    1,
    async ({ codeInstrumentationId, resultIds }) => {
      if (!codeInstrumentationId || !utility.isArrayType(resultIds)) {
        return Promise.resolve();
      }
      return Promise.all(resultIds.map(async (resultId) => {
        if (resultStatusMap[resultId] !== resultStatusKind.success) {
          return Promise.resolve();
        }
        // 不收集覆盖率的情况下，确保测试用例map有值
        const results = await CodeInstrumentationResult.find({
          codeInstrumentationId: Types.ObjectId(codeInstrumentationId),
          resultId: Types.ObjectId(resultId),
        }, ['testcaseId']);
        results.forEach((result) => {
          testcasesMap.set(result.testcaseId.toString(), {
            codeInstrumentationId,
            resultId,
          });
        });
        const resultCovereds = await getResultCovered(codeInstrumentationId, resultId);
        resultCovereds.forEach((resultCovered) => {
          let funcKey = resultCovered.functionId.toString();
          if (!functionsMap.has(funcKey)
            || !resultCovered.coveredFields
            || resultCovered.functionHash !== functionsMap.get(funcKey).hash) {
            return;
          }
          if (resultCovered.testcaseId) {
            if (projectVersion.versionType === projectVersions.versionType.integrationTest) {
              funcKey = resultCovered.entryFuncId.toString();
            }
            testcasesMap.set(resultCovered.testcaseId.toString(), {
              codeInstrumentationId,
              resultId,
              functionId: funcKey,
            });
            if (functionsMap.get(funcKey).testcasesCoveredFields[resultCovered.testcaseId] === undefined) {
              functionsMap.get(funcKey).testcasesCoveredFields[resultCovered.testcaseId] = {};
            }
            Object.keys(resultCovered.coveredFields).forEach((key) => {
              // 集成测试只合集成函数的调用覆盖和函数覆盖
              if (projectVersion.versionType === projectVersions.versionType.integrationTest
                && resultCovered.functionId.toString() !== resultCovered.entryFuncId.toString()
                && !key.startsWith('invokes')) {
                return;
              }
              functionsMap.get(funcKey).testcasesCoveredFields[resultCovered.testcaseId][key] = resultCovered.coveredFields[key];
            });
          } else {
            Object.keys(resultCovered.coveredFields).forEach((key) => {
              if (functionsMap.get(funcKey).coveredFields[key] !== undefined) {
                functionsMap.get(funcKey).coveredFields[key] += resultCovered.coveredFields[key];
              } else {
                functionsMap.get(funcKey).coveredFields[key] = resultCovered.coveredFields[key];
              }
            });
          }
        });
        return Promise.resolve();
      }));
    }
  );
  await utility.arrayChunkOperation([...testcasesMap.keys()], 10, async (testId) => {
    const testcase = await tests.getTest(testId);
    if (!testcase) {
      if (testcasesMap.get(testId).functionId) {
        delete functionsMap.get(testcasesMap.get(testId).functionId).testcasesCoveredFields[testId];
      }
      testcasesMap.delete(testId);
      return Promise.resolve();
    }
    let testcaseResult = await CodeInstrumentationResult.findOne({
      codeInstrumentationId: Types.ObjectId(testcasesMap.get(testId).codeInstrumentationId),
      resultId: Types.ObjectId(testcasesMap.get(testId).resultId),
      testcaseId: Types.ObjectId(testId),
    });
    if (!testcaseResult || !testcaseResult.output) {
      testcaseResult = { output: {} };
    }
    if (!testcase.data) {
      testcase.data = {};
    }
    if (!testcase.data.output) {
      testcase.data.output = {};
    }
    // testCase中的原有预期值
    const expectValues = tests.getTestCaseValues(testcase.data.output, '@value', 'data.output');
    // content中的实际值
    const resultValues = tests.getTestCaseValues(testcaseResult.output, '@value', 'data.output');
    // inRange在范围内的值
    const inRangeValues = tests.getTestCaseValues(testcaseResult.output, '@inRange', 'data.output');
    // 桩函数参数检查项
    const matchedValues = tests.getTestCaseValues(testcaseResult.output, '@isMatch', 'data.output');
    tests.removeOutputResult(testcase.data.output);
    testcase.data.output.unused = testcaseResult.output.unused || {};
    if (testcaseResult.hash && testcaseResult.hash !== testcase.hash) {
      delete functionsMap.get(testcase.functionId.toString()).testcasesCoveredFields[testId];
      testcase.resultKind = tests.resultKind.unknown;
      testcase.error = true;
      testcasesMap.set(testId, testcase);
      functionsMap.get(testcase.functionId.toString()).testcases.add(testId);
      return Promise.resolve();
    }
    let resultKind = tests.resultKind.succeed;
    // 将桩函数参数检查项的所有实际值设置成？
    Object.keys(expectValues).forEach((field) => {
      if (!field.match(/^data\.output\.stubs|integratedFuncs\.[^.]+\.@value\..+\.@value$/)) {
        return;
      }
      if (!resultValues[field]
        && !matchedValues[`${field.slice(0, -'value'.length)}isMatch`]
        && expectValues[field].substr(0, 1) !== '@') {
        resultValues[field] = '?';
      }
    });
    Object.keys(resultValues).forEach((field) => {
      const fieldPath = field.slice(0, -'value'.length);
      if (expectValues[field]
        && expectValues[field] !== '?'
        && (inRangeValues[`${fieldPath}inRange`] === 0
          || (inRangeValues[`${fieldPath}inRange`] === undefined && !matchedValues[`${fieldPath}isMatch`]))
      ) {
        resultKind = tests.resultKind.failed;
        utility.setValueByKeyPath(testcase, `${fieldPath}isEqual`, false);
      }
      utility.setValueByKeyPath(testcase, `${fieldPath}result`, resultValues[field]);
      // 如果自动填充实际值开启并且没有设置预期值，将实际值赋给预期值
      if (projectVersion.autoFill && expectValues[field] === undefined) {
        utility.setValueByKeyPath(testcase, `${fieldPath}value`, resultValues[field]);
        expectValues[field] = resultValues[field];
      }
      // 预期值填写'?'和不填预期值结果等价
      if (expectValues[field] && expectValues[field] === '?') {
        delete expectValues[field];
      }
    });
    // 没有expectValues意味着没有填写任何预期值，如果执行成功，更新结果为无结果或者通过×
    if (resultKind === tests.resultKind.succeed && !Object.keys(expectValues).length) {
      resultKind = projectVersion.strictOutputCheck === false
        ? tests.resultKind.nonStrictSucceed : tests.resultKind.noResult;
    }

    // 实际调用次数记录到testCase表
    const { output } = testcaseResult;
    let stubKeys = [];
    if (testcase.data && testcase.data.output && testcase.data.output.stubs) {
      stubKeys = Object.keys(testcase.data.output.stubs);
    }
    stubKeys.forEach((stubName) => {
      // 没有调用到的桩函数默认调用次数是0
      const count = output.stubs && output.stubs[stubName] ? (output.stubs[stubName]['@count'] || 0) : 0;
      // doc.$set[`data.output.stubs.${stubName}.@count`] = count;
      utility.setValueByKeyPath(testcase, `data.output.stubs.${stubName}.@count`, count);
      // 桩函数调用次数导致执行结果变化特殊处理
      const expectedCount = utility.getValueByKeyPath(testcase.data.output, `stubs.${stubName}.@countExpected`);
      if (expectedCount !== undefined
        && (resultKind === tests.resultKind.succeed
          || resultKind === tests.resultKind.nonStrictSucceed
          || resultKind === tests.resultKind.noResult)
      ) {
        // 设置了桩函数调用次数预期值，执行结果要改为成功或失败，不能为无结果或者通过*
        resultKind = parseInt(expectedCount, 10) === count ? tests.resultKind.succeed : tests.resultKind.failed;
      }
    });
    if (testcaseResult.error) {
      resultKind = tests.resultKind.timeout;
    }
    testcase.resultKind = resultKind;
    testcasesMap.set(testId, testcase);
    functionsMap.get(testcase.functionId.toString()).testcases.add(testId);
    return Promise.resolve();
  });
  if (!includeCoverage) {
    return {
      filesMap,
      functionsMap,
      testcasesMap,
      mode,
    };
  }
  const insideProjectFuncIds = new Set();
  if (projectVersion.versionType === projectVersions.versionType.integrationTest) {
    const funcs = await Func.find(
      {
        versionId: projectVersion._id,
        kind: { $in: [fileElements.elementKind.function, fileElements.elementKind.specialFunction] },
        insideProject: true,
      },
      { _id: 1 }
    ).lean();
    funcs.forEach((func) => {
      insideProjectFuncIds.add(func._id.toString());
    });
  }
  await utility.arrayChunkOperation([...functionsMap.keys()], 10, async (key) => {
    const func = functionsMap.get(key);
    if (!filesMap.has(func.fileId.toString())) {
      return;
    }
    let funcCfg = await functions.getFunctionCFG(func._id, null, false);
    ['nodes', 'edges', 'branches', 'decisions'].forEach((key) => {
      if (!funcCfg[key]) {
        funcCfg[key] = {};
      }
    });
    funcCfg = functions.mergeCfgCovered(funcCfg, func.coveredFields);
    Object.keys(func.testcasesCoveredFields).forEach((testId) => {
      funcCfg = functions.mergeCfgCovered(funcCfg, func.testcasesCoveredFields[testId], testId);
    });
    if (projectVersion.mcdcKind === tests.mcdcKind.masking) {
      funcCfg = functions.completeDecisionCases(funcCfg, tests.mcdcKind.masking);
    }
    const {
      statementCovered,
      statementTotal,
      branchCovered,
      branchTotal,
      conditionCovered,
      conditionTotal,
      decisionCovered,
      decisionTotal,
      mcdcCovered,
      mcdcTotal,
      tests: testcaseCoverages,
    } = functions.getCoverage(funcCfg, projectVersion.mcdcKind);
    func.statementCovered = statementCovered;
    func.statementTotal = statementTotal;
    func.branchCovered = branchCovered;
    func.branchTotal = branchTotal;
    func.conditionCovered = conditionCovered;
    func.conditionTotal = conditionTotal;
    func.decisionCovered = decisionCovered;
    func.decisionTotal = decisionTotal;
    func.mcdcCovered = mcdcCovered;
    func.mcdcTotal = mcdcTotal;
    let invokeCovered = 0;
    let invokeTotal = 0;
    let invokedFuncCovered = 0;
    let invokedFuncTotal = 0;
    if (projectVersion.versionType === projectVersions.versionType.integrationTest) {
      const relations = await invokeRelations.retrieveRelations(func._id, { includeCtor: false });
      const toFuncIdSet = new Set();
      toFuncIdSet.add(func._id.toString());
      const stubMangled = new Set();
      relations.forEach((relation) => {
        // 插桩函数过滤
        if (relation.enableStub) {
          stubMangled.add(relation.toFuncMangledId);
          return;
        }
        // 未定义函数过滤
        if (!relation.toFuncId) {
          return;
        }
        // 函数指针过滤
        if (relation.toFuncPointer === true || relation.toFuncCtor === true) {
          return;
        }
        // 项目外函数过滤
        if (!insideProjectFuncIds.has(relation.toFuncId.toString())) {
          return;
        }
        toFuncIdSet.add(relation.toFuncId.toString());
      });
      const callExprs = await FunctionCFG.find(
        {
          functionId: { $in: [...toFuncIdSet].map((id) => Types.ObjectId(id)) },
          callexpr: { $exists: true },
        }
      );
      callExprs.forEach((callExpr) => {
        if (funcCfg.invokes && funcCfg.invokes[callExpr.functionId.toString()]) {
          Object.keys(funcCfg.invokes[callExpr.functionId.toString()].testIds).forEach((testId) => {
            if (!testcaseCoverages[testId]) {
              testcaseCoverages[testId] = {};
            }
            if (testcaseCoverages[testId].invokedFuncCovered === undefined) {
              testcaseCoverages[testId].invokedFuncCovered = 0;
            }
            testcaseCoverages[testId].invokedFuncCovered += 1;
          });
          invokedFuncCovered += 1;
        }
        if (!utility.isArrayType(callExpr.callexpr)) {
          return;
        }
        callExpr.callexpr.forEach((expr) => {
          // 打桩函数不计入调用覆盖
          if (expr.mangled && stubMangled.has(expr.mangled)) {
            return;
          }
          if (funcCfg.invokes
            && funcCfg.invokes[callExpr.functionId.toString()]
            && funcCfg.invokes[callExpr.functionId.toString()].callexpr
            && funcCfg.invokes[callExpr.functionId.toString()].callexpr[expr.id]
            && funcCfg.invokes[callExpr.functionId.toString()].callexpr[expr.id].testIds
          ) {
            Object.keys(funcCfg.invokes[callExpr.functionId.toString()].callexpr[expr.id].testIds).forEach((testId) => {
              if (!testcaseCoverages[testId]) {
                testcaseCoverages[testId] = {};
              }
              if (testcaseCoverages[testId].invokeCovered === undefined) {
                testcaseCoverages[testId].invokeCovered = 0;
              }
              testcaseCoverages[testId].invokeCovered += 1;
            });
            invokeCovered += 1;
          }
          invokeTotal += 1;
        });
      });
      invokedFuncTotal = toFuncIdSet.size;
      func.invokedFuncCovered = invokedFuncCovered;
      func.invokeTotal = invokeTotal;
      func.invokeCovered = invokeCovered;
      func.invokedFuncTotal = invokedFuncTotal;
    }
    [...func.testcases].forEach((testId) => {
      testcasesMap.get(testId).statementCovered = testcaseCoverages[testId] ? testcaseCoverages[testId].statementCovered : 0;
      testcasesMap.get(testId).branchCovered = testcaseCoverages[testId] ? testcaseCoverages[testId].branchCovered : 0;
      testcasesMap.get(testId).conditionCovered = testcaseCoverages[testId] ? testcaseCoverages[testId].conditionCovered : 0;
      testcasesMap.get(testId).decisionCovered = testcaseCoverages[testId] ? testcaseCoverages[testId].decisionCovered : 0;
      testcasesMap.get(testId).mcdcCovered = testcaseCoverages[testId] ? testcaseCoverages[testId].mcdcCovered : 0;
      testcasesMap.get(testId).invokeCovered = testcaseCoverages[testId] ? testcaseCoverages[testId].invokeCovered : 0;
      testcasesMap.get(testId).invokedFuncCovered = testcaseCoverages[testId] ? testcaseCoverages[testId].invokedFuncCovered : 0;
    });
    const fileInfo = filesMap.get(func.fileId.toString());
    func.filePath = fileInfo.path;
    const extname = common.renameMongoKey(fileInfo.extname);
    fileInfo.statementCovered[extname] += statementCovered;
    fileInfo.statementTotal[extname] += statementTotal;
    fileInfo.branchCovered[extname] += branchCovered;
    fileInfo.branchTotal[extname] += branchTotal;
    fileInfo.conditionCovered[extname] += conditionCovered;
    fileInfo.conditionTotal[extname] += conditionTotal;
    fileInfo.decisionCovered[extname] += decisionCovered;
    fileInfo.decisionTotal[extname] += decisionTotal;
    fileInfo.mcdcCovered[extname] += mcdcCovered;
    fileInfo.mcdcTotal[extname] += mcdcTotal;
    fileInfo.invokeCovered[extname] += invokeCovered;
    fileInfo.invokeTotal[extname] += invokeTotal;
    fileInfo.invokedFuncCovered[extname] += invokedFuncCovered;
    fileInfo.invokedFuncTotal[extname] += invokedFuncTotal;
  });
  return {
    filesMap,
    functionsMap,
    testcasesMap,
    mode,
  };
}

async function getAnalyzedFileTree(codeInsAndResults, projectVersion) {
  const { filesMap, functionsMap } = await analyzeResults(codeInsAndResults, projectVersion);
  const dirs = await File.find(
    {
      versionId: projectVersion._id,
      kind: filesModels.kind.directory,
    },
    ['fileName', 'path', 'directoryId', 'kind']
  ).lean();
  const dirsMap = {};
  dirs.forEach((codeInsFile) => {
    dirsMap[codeInsFile._id.toString()] = codeInsFile;
  });
  let parentIds = [];
  functionsMap.forEach((func) => {
    if (!func.parents) {
      return;
    }
    func.parents.forEach((parentId, index) => {
      if (index === 0) {
        return;
      }
      parentIds.push(parentId.toString());
    });
  });
  parentIds = utility.arrayDeduplicate(parentIds).map((id) => Types.ObjectId(id));
  const parentFuncs = await Func.find({ versionId: projectVersion._id, _id: { $in: parentIds } }, ['functionName', 'kind', 'fileId']).lean();
  const parentFuncsMap = {};
  parentFuncs.forEach((parentFunc) => {
    parentFuncsMap[parentFunc._id.toString()] = parentFunc;
  });
  const fileTreeElementMap = {};
  const linkedParentMap = {};
  filesMap.forEach((file) => {
    const fileId = file._id.toString();
    const extname = common.renameMongoKey(file.extname);
    fileTreeElementMap[fileId] = {
      id: fileId,
      name: file.fileName,
      path: file.path,
      kind: filesModels.kind.file,
      statementCovered: file.statementCovered[extname],
      statementTotal: file.statementTotal[extname],
      branchCovered: file.branchCovered[extname],
      branchTotal: file.branchTotal[extname],
      mcdcCovered: file.mcdcCovered[extname],
      mcdcTotal: file.mcdcTotal[extname],
      conditionCovered: file.conditionCovered[extname],
      conditionTotal: file.conditionTotal[extname],
      decisionCovered: file.decisionCovered[extname],
      decisionTotal: file.decisionTotal[extname],
      invokeCovered: file.invokeCovered[extname],
      invokeTotal: file.invokeTotal[extname],
      invokedFuncCovered: file.invokedFuncCovered[extname],
      invokedFuncTotal: file.invokedFuncTotal[extname],
      children: [],
    };
    let currentElement = fileTreeElementMap[fileId];
    let directoryId = file.directoryId.toString();
    while (directoryId) {
      if (!dirsMap[directoryId]) {
        break;
      }
      const dir = dirsMap[directoryId];
      if (!fileTreeElementMap[directoryId]) {
        fileTreeElementMap[directoryId] = {
          id: directoryId,
          name: dir.fileName,
          path: dir.path,
          kind: filesModels.kind.directory,
          statementCovered: 0,
          statementTotal: 0,
          branchCovered: 0,
          branchTotal: 0,
          mcdcCovered: 0,
          mcdcTotal: 0,
          conditionCovered: 0,
          conditionTotal: 0,
          decisionCovered: 0,
          decisionTotal: 0,
          invokeCovered: 0,
          invokeTotal: 0,
          invokedFuncCovered: 0,
          invokedFuncTotal: 0,
          children: [],
        };
      }
      if (!linkedParentMap[currentElement.id]) {
        fileTreeElementMap[directoryId].children.push(currentElement);
        linkedParentMap[currentElement.id] = 1;
      }
      fileTreeElementMap[directoryId].statementCovered += fileTreeElementMap[fileId].statementCovered;
      fileTreeElementMap[directoryId].statementTotal += fileTreeElementMap[fileId].statementTotal;
      fileTreeElementMap[directoryId].branchCovered += fileTreeElementMap[fileId].branchCovered;
      fileTreeElementMap[directoryId].branchTotal += fileTreeElementMap[fileId].branchTotal;
      fileTreeElementMap[directoryId].mcdcCovered += fileTreeElementMap[fileId].mcdcCovered;
      fileTreeElementMap[directoryId].mcdcTotal += fileTreeElementMap[fileId].mcdcTotal;
      fileTreeElementMap[directoryId].conditionCovered += fileTreeElementMap[fileId].conditionCovered;
      fileTreeElementMap[directoryId].conditionTotal += fileTreeElementMap[fileId].conditionTotal;
      fileTreeElementMap[directoryId].decisionCovered += fileTreeElementMap[fileId].decisionCovered;
      fileTreeElementMap[directoryId].decisionTotal += fileTreeElementMap[fileId].decisionTotal;
      fileTreeElementMap[directoryId].invokeCovered += fileTreeElementMap[fileId].invokeCovered;
      fileTreeElementMap[directoryId].invokeTotal += fileTreeElementMap[fileId].invokeTotal;
      fileTreeElementMap[directoryId].invokedFuncCovered += fileTreeElementMap[fileId].invokedFuncCovered;
      fileTreeElementMap[directoryId].invokedFuncTotal += fileTreeElementMap[fileId].invokedFuncTotal;
      currentElement = fileTreeElementMap[directoryId];
      directoryId = dir.directoryId ? dir.directoryId.toString() : false;
    }
  });
  functionsMap.forEach((func) => {
    const funcId = func._id.toString();
    fileTreeElementMap[funcId] = {
      id: funcId,
      fileId: func.fileId,
      name: func.functionName,
      kind: func.kind,
      location: func.location,
      statementCovered: func.statementCovered,
      statementTotal: func.statementTotal,
      branchCovered: func.branchCovered,
      branchTotal: func.branchTotal,
      mcdcCovered: func.mcdcCovered,
      mcdcTotal: func.mcdcTotal,
      conditionCovered: func.conditionCovered,
      conditionTotal: func.conditionTotal,
      decisionCovered: func.decisionCovered,
      decisionTotal: func.decisionTotal,
      invokeCovered: func.invokeCovered,
      invokeTotal: func.invokeTotal,
      invokedFuncCovered: func.invokedFuncCovered,
      invokedFuncTotal: func.invokedFuncTotal,
    };
    let currentElement = fileTreeElementMap[funcId];
    let parentId = func.parents.pop();
    while (parentId) {
      parentId = parentId.toString();
      if (!parentFuncsMap[parentId]) {
        break;
      }
      const parent = parentFuncsMap[parentId];
      if (!fileTreeElementMap[parentId]) {
        fileTreeElementMap[parentId] = {
          id: parentId,
          fileId: parent.fileId,
          name: parent.functionName,
          kind: parent.kind,
          location: parent.location,
          statementCovered: 0,
          statementTotal: 0,
          branchCovered: 0,
          branchTotal: 0,
          mcdcCovered: 0,
          mcdcTotal: 0,
          conditionCovered: 0,
          conditionTotal: 0,
          decisionCovered: 0,
          decisionTotal: 0,
          invokeCovered: 0,
          invokeTotal: 0,
          invokedFuncCovered: 0,
          invokedFuncTotal: 0,
          children: [],
        };
      }
      if (!linkedParentMap[currentElement.id]) {
        fileTreeElementMap[parentId].children.push(currentElement);
        linkedParentMap[currentElement.id] = 1;
      }
      fileTreeElementMap[parentId].statementCovered += fileTreeElementMap[funcId].statementCovered;
      fileTreeElementMap[parentId].statementTotal += fileTreeElementMap[funcId].statementTotal;
      fileTreeElementMap[parentId].branchCovered += fileTreeElementMap[funcId].branchCovered;
      fileTreeElementMap[parentId].branchTotal += fileTreeElementMap[funcId].branchTotal;
      fileTreeElementMap[parentId].mcdcCovered += fileTreeElementMap[funcId].mcdcCovered;
      fileTreeElementMap[parentId].mcdcTotal += fileTreeElementMap[funcId].mcdcTotal;
      fileTreeElementMap[parentId].conditionCovered += fileTreeElementMap[funcId].conditionCovered;
      fileTreeElementMap[parentId].conditionTotal += fileTreeElementMap[funcId].conditionTotal;
      fileTreeElementMap[parentId].decisionCovered += fileTreeElementMap[funcId].decisionCovered;
      fileTreeElementMap[parentId].decisionTotal += fileTreeElementMap[funcId].decisionTotal;
      fileTreeElementMap[parentId].invokeCovered += fileTreeElementMap[funcId].invokeCovered;
      fileTreeElementMap[parentId].invokeTotal += fileTreeElementMap[funcId].invokeTotal;
      fileTreeElementMap[parentId].invokedFuncCovered += fileTreeElementMap[funcId].invokedFuncCovered;
      fileTreeElementMap[parentId].invokedFuncTotal += fileTreeElementMap[funcId].invokedFuncTotal;
      currentElement = fileTreeElementMap[parentId];
      parentId = func.parents.pop();
    }
    if (fileTreeElementMap[func.fileId.toString()]) {
      fileTreeElementMap[func.fileId.toString()].children.push(currentElement);
    }
  });
  // 计算覆盖率和total
  let total = 0;
  function step(element) {
    total += 1;
    element.statementCoverage = element.statementTotal ? element.statementCovered / element.statementTotal : null;
    element.branchCoverage = element.branchTotal ? element.branchCovered / element.branchTotal : null;
    element.mcdcCoverage = element.mcdcTotal ? element.mcdcCovered / element.mcdcTotal : null;
    element.conditionCoverage = element.conditionTotal ? element.conditionCovered / element.conditionTotal : null;
    element.decisionCoverage = element.decisionTotal ? element.decisionCovered / element.decisionTotal : null;
    element.invokeCoverage = element.invokeTotal ? element.invokeCovered / element.invokeTotal : null;
    element.invokedFuncCoverage = element.invokedFuncTotal ? element.invokedFuncCovered / element.invokedFuncTotal : null;
    if (utility.isArrayType(element.children) && element.children.length > 0) {
      element.children.forEach((children) => {
        step(children);
      });
    }
  }
  step(fileTreeElementMap[projectVersion.rootDirectoryId.toString()]);
  return {
    tree: fileTreeElementMap[projectVersion.rootDirectoryId.toString()],
    total,
  };
}

async function getFunctionCfg(functionId, codeInsAndResults, projectVersion) {
  const func = await functions.getFunction(functionId);
  if (!func) {
    return {};
  }
  let funcCfg = await functions.getFunctionCFG(functionId, null, false);
  if (!funcCfg) {
    return {};
  }
  const resultStatusMap = {};
  await Promise.all(codeInsAndResults.map(async ({ codeInstrumentationId }) => {
    if (codeInstrumentationId) {
      const codeInstrumentation = await getCodeInstrumentation(codeInstrumentationId);
      codeInstrumentation.results.forEach((result) => {
        resultStatusMap[result._id.toString()] = result.status;
      });
    }
  }));
  const coveredFields = {};
  await utility.arrayChunkOperation(
    codeInsAndResults,
    1,
    async ({ codeInstrumentationId, resultIds }) => {
      if (!codeInstrumentationId || !utility.isArrayType(resultIds)) {
        return Promise.resolve();
      }
      return Promise.all(resultIds.map(async (resultId) => {
        if (resultStatusMap[resultId] !== resultStatusKind.success) {
          return Promise.resolve();
        }
        const resultCovereds = await getResultCovered(codeInstrumentationId, resultId, func._id);
        resultCovereds.forEach((resultCovered) => {
          if (!resultCovered.coveredFields || resultCovered.functionHash !== func.hash) {
            return;
          }
          Object.keys(resultCovered.coveredFields).forEach((key) => {
            if (coveredFields[key] !== undefined) {
              coveredFields[key] += resultCovered.coveredFields[key];
            } else {
              coveredFields[key] = resultCovered.coveredFields[key];
            }
          });
        });
        return Promise.resolve();
      }));
    }
  );
  funcCfg = functions.mergeCfgCovered(funcCfg, coveredFields);
  return functions.completeDecisionCases(funcCfg, projectVersion.mcdcKind);
}
async function storeResultOutput(outputMap, codeInstrumentationId, resultId, projectVersion, error, hash) {
  const batchUpdate = [];
  outputMap.forEach((output, testcaseId) => {
    batchUpdate.push({
      updateOne: {
        filter: { resultId: Types.ObjectId(resultId), testcaseId: Types.ObjectId(testcaseId) },
        update: {
          $set: {
            output,
            codeInstrumentationId: Types.ObjectId(codeInstrumentationId),
            versionId: projectVersion._id,
            error,
            hash,
          },
        },
        upsert: true,
      },
    });
  });
  return CodeInstrumentationResult.bulkWrite(batchUpdate);
}
async function storeResultCovered(content, codeInstrumentationId, resultId, projectVersion, testIds) {
  let entryFuncId = null;
  if (testIds && testIds.length > 0) {
    const test = await tests.getTest(testIds[0]);
    if (!test) {
      return Promise.resolve();
    }
    entryFuncId = test.functionId;
  }
  if (!content.coverage || !content.coverage.functions || content.coverage.functions.length === 0) {
    return Promise.resolve();
  }
  return Promise.all(content.coverage.functions.map(async (func) => {
    func.blocks = func.blocks || [];
    func.branches = func.branches || [];
    func.decisions = func.decisions || [];
    const coveredFields = {};
    // blocks
    func.blocks.forEach((block) => {
      // 节点覆盖情况
      if (block.count) {
        const field = `nodes|${block.id}`;
        coveredFields[field] = block.count;
      }
      if (block.statements) {
        block.statements.forEach((statement) => {
          if (statement.count) {
            const field = `nodes|${block.id}|statements|${statement.id}`;
            coveredFields[field] = statement.count;
          }
        });
      }
      if (block.succs) {
        block.succs.forEach((succ) => {
          if (succ.count) {
            const field = `edges|${functions.getEdgeKey(block.id, succ.id)}`;
            coveredFields[field] = succ.count;
          }
        });
      }
    });
    // branches
    func.branches.forEach((branch) => {
      branch.succs.forEach((item) => {
        let key = item.block.toString();
        if (item.sentence !== undefined) {
          key = `${key}-${item.sentence}`;
        }
        if (item.count) {
          const field = `branches|${branch.id}|branches|${key}`;
          coveredFields[field] = item.count;
        }
      });
    });
    // decisions
    func.decisions.forEach((decision) => {
      if (!utility.isEmpty(decision.evaluates)) {
        decision.evaluates.forEach((item) => {
          item.testcases = testIds || ['1'];
        });
      }
      const coveredConditions = functions.getCoveredConditions(decision);
      Object.keys(coveredConditions).forEach((coveredCondition) => {
        Object.keys(coveredConditions[coveredCondition].testIds).forEach((testId) => {
          const field = `decisions|${decision.id}|coveredConditions|${coveredCondition}`;
          coveredFields[field] = coveredConditions[coveredCondition].testIds[testId];
        });
      });
    });
    const file = await filesModels.getFileByPath(
      projectVersion._id,
      func.path ? func.path : (func.location ? func.location.path : '')// eslint-disable-line no-nested-ternary
    );
    if (!file) {
      return Promise.resolve();
    }
    const funcDb = await functions.getFunctionByKey(file._id, func.mangled);
    if (!funcDb) {
      return Promise.resolve();
    }
    if (projectVersion.versionType === projectVersions.versionType.integrationTest) {
      if (func.count) {
        const field = `invokes|${funcDb._id}`;
        coveredFields[field] = func.count;
      }
      if (func.callexpr && func.callexpr.length > 0) {
        func.callexpr.forEach((expr) => {
          if (expr.count) {
            const field = `invokes|${funcDb._id}|callexpr|${expr.id}`;
            coveredFields[field] = expr.count;
          }
        });
      }
    }
    if (utility.isEmpty(coveredFields)) {
      return Promise.resolve();
    }
    if (!testIds) {
      testIds = [undefined];
    }
    return CodeInstrumentationCovered.insertMany(testIds.map((testId) => ({
      versionId: projectVersion._id,
      codeInstrumentationId: Types.ObjectId(codeInstrumentationId),
      resultId: Types.ObjectId(resultId),
      fileId: file._id,
      functionId: funcDb._id,
      functionHash: funcDb.hash,
      entryFuncId,
      coveredFields,
      testcaseId: testId,
    })));
  }));
}
async function updateStatus(codeInstrumentationId, status) {
  return CodeInstrumentation.updateOne({ _id: Types.ObjectId(codeInstrumentationId) }, { $set: { status } });
}

async function updateResultStatus(codeInstrumentationId, resultId, status) {
  const codeIns = await CodeInstrumentation.findOneAndUpdate(
    { _id: Types.ObjectId(codeInstrumentationId), results: { $elemMatch: { _id: Types.ObjectId(resultId) } } },
    { $set: { 'results.$.status': status } },
    { new: true }
  );
  publisher.publishCodeInstrumentationMessage('update', {
    codeInstrumentationId,
    resultId,
    status,
    versionId: codeIns.versionId,
    creatorId: codeIns.creatorId,
  });
}

async function importAnalyzedResults(codeInsAndResults, projectVersion) {
  const funcIdsSet = new Set();
  // 获取上一次黑盒影响的函数id
  const blackBoxFuncs = await BlackBoxCovered.find({ versionId: projectVersion._id }, ['functionId']);
  blackBoxFuncs.forEach((blackBoxFunc) => {
    funcIdsSet.add(blackBoxFunc.functionId.toString());
  });
  // 删除上一次黑盒的覆盖率
  await BlackBoxCovered.deleteMany({ versionId: projectVersion._id });
  // 清空上一次选择的结果
  await CodeInstrumentation.updateMany({ versionId: projectVersion._id }, { $unset: { analyzedResults: 1 } });
  // 记录本次选中的结果
  const batchUpdate = [];
  codeInsAndResults.forEach(({ codeInstrumentationId, resultIds }) => {
    batchUpdate.push({
      updateOne: {
        filter: { _id: Types.ObjectId(codeInstrumentationId) },
        update: { $set: { analyzedResults: resultIds } },
      },
    });
  });
  if (batchUpdate.length > 0) {
    await CodeInstrumentation.bulkWrite(batchUpdate);
  }
  const { functionsMap } = await analyzeResults(codeInsAndResults, projectVersion);
  // 插入当前的黑盒覆盖率
  const docs = [];
  functionsMap.forEach((func) => {
    if (utility.isEmpty(func.coveredFields)) {
      return;
    }
    docs.push({
      versionId: projectVersion._id,
      fileId: func.fileId,
      functionId: func._id,
      coveredFields: func.coveredFields,
    });
    funcIdsSet.add(func._id.toString());
  });
  if (docs.length > 0) {
    await BlackBoxCovered.collection.insertMany(docs, { ordered: false });
  }
  // 更新影响到的函数覆盖率
  return utility.arrayChunkOperation([...funcIdsSet], 10, async (functionId) => {
    await functions.updateMcdcCases(functionId, projectVersion.mcdcKind);
    return functions.updateFunctionCoverage(functionId);
  });
}

eventHub.on(eventKind.EVENT_VERSION_REMOVE, async (versionId) => {
  versionId = Types.ObjectId(versionId);
  await Promise.all([
    CodeInstrumentation.deleteMany({ versionId }),
    CodeInstrumentationCovered.deleteMany({ versionId }),
    CodeInstrumentationResult.deleteMany({ versionId }),
    // 清除插桩代码目录
    fs.remove(path.join(cacheUtil.getCodeInstrumentationDir(), versionId.toString())),
  ]);
});

module.exports = {
  type,
  statusKind,
  resultStatusKind,
  analyzeResults,
  codeInstrumentationList,
  codeInstrumentationEdit,
  createCodeInstrumentation,
  getAnalyzedFileTree,
  getCodeInstrumentation,
  getCodeInstrumentationTree,
  getFileCodeInstrumentationFilePath,
  getFilesAndFunctions,
  getFunctionCfg,
  getResultCovered,
  importAnalyzedResults,
  uploadResultFile,
  updateResultStatus,
  updateStatus,
  removeCodeInstrumentation,
  removeCodeInstrumentationResult,
  storeResultCovered,
};
exports = Object.assign(exports, module.exports);
